﻿//==============================================================================
// File: PropertyContainer.cs
// Created: 2010-05-10
// Author: Piotr Włodek
//==============================================================================
// This file is a part of MAMMOTH project.
// Copyright (C) 2010 AGH University of Science and Technology, Krakow.
// https://caribou.iisg.agh.edu.pl/trac/mammoth
//
// Mammoth is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// 
// Mammoth is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.
// 
// You should have received a copy of the GNU Lesser General Public License
// along with WpfRcp. If not, see http://www.gnu.org/licenses/.
//==============================================================================

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Runtime.Serialization;
using System.Text;
using Castle.DynamicProxy;
using WpfRcp.PropertyModel.Core;
using WpfRcp.PropertyModel.Internal.Associations;
using WpfRcp.PropertyModel.Internal.Common;
using WpfRcp.PropertyModel.Scope;
using WpfRcp.PropertyModel.Validation;
using Res = WpfRcp.PropertyModel.Properties.Resources;

namespace WpfRcp.PropertyModel
{
    /// <summary>
    /// Represents default <see cref="IPropertyContainer"/> implementation.
    /// </summary>
    [Serializable]
    public class PropertyContainer : 
        IPropertyContainer,
        INotifyPropertyChanging,
        INotifyPropertyChanged,
        IDataErrorInfo,
        IEditableObject,
        IDisposable
    {
        private readonly IMetaPropertyManager m_MetaPropertyManager;
        private readonly IObjectRuleList m_ObjectRuleList;
        private readonly Type m_ThisType;

        [NonSerialized]
        private ValidationResultCollection m_ValidationResults;

        [NonSerialized]
        private PropertyContainerType m_Type;

        /// <summary>
        /// Initializes a new instance of the <see cref="PropertyContainer"/> class.
        /// </summary>
        /// <param name="validate">Indicates if the validation should occure during constructor call.</param>
        /// <param name="strategy">Setter strategy.</param>
        protected PropertyContainer(bool validate, SetterStrategy strategy)
            : this(null, validate)
        {
            // ReSharper disable DoNotCallOverridableMethodsInConstructor
            
            Setter = strategy;

            // ReSharper restore DoNotCallOverridableMethodsInConstructor
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="PropertyContainer"/> class.
        /// </summary>
        protected PropertyContainer() : this(null, true)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="PropertyContainer"/> class.
        /// </summary>
        /// <param name="parent">Parent container.</param>
        /// <param name="validate">Indicates if the validation should occure during constructor call.</param>
        /// <remarks>
        /// If parent is specified, all internal events will contain the given parent
        /// rather than this instance. This is primary used for scenarios, where the developer
        /// does not want to (or can't) derive from <see cref="PropertyContainer"/> class.
        /// </remarks>
        public PropertyContainer(IPropertyContainer parent, bool validate)
        {
            // If parent is null, we treat ourself as a parent
            if (parent == null)
            {
                parent = this;
            }

            m_ThisType = parent.GetType();
            m_ValidationResults = new ValidationResultCollection();

            // ReSharper disable DoNotCallOverridableMethodsInConstructor

            m_MetaPropertyManager = GetMetaPropertyManagerOverride(parent);
            if (m_MetaPropertyManager == null)
                throw new PropertyContainerException(Res.ExceptionMetaPropertyManagerIsNull);

            m_ObjectRuleList = GetObjectRuleListOverride();
            if (m_ObjectRuleList == null)
                throw new PropertyContainerException(Res.ExceptionObjectRuleListIsNull);

            OnAddObjectValidators();

            OnInitialize();

            if (validate)
                Validate();

            // ReSharper restore DoNotCallOverridableMethodsInConstructor
        }

        /// <summary>
        /// Gets a <see cref="PropertyContainerType"/> which represents this type.
        /// </summary>
        public PropertyContainerType PropertyContainerType
        {
            get
            {
                if (m_Type == null)
                    m_Type = PropertyContainerType.FromType(m_ThisType);

                return m_Type;
            }
        }

        #region Public Indexers

        /// <summary>
        /// Gets or sets a value of a given property.
        /// </summary>
        /// <param name="index">Property for which value is being retrieved.</param>
        /// <returns>Value of a given property.</returns>
        public object this[MetaProperty index]
        {
            get
            {
                ThrowIfDisposed();
                return GetValue(index);
            }
            set
            {
                ThrowIfDisposed();
                SetValue(index, value);
            }
        }

        /// <summary>
        /// Gets or sets a value of a given property.
        /// </summary>
        /// <param name="index">Property for which value is being retrieved.</param>
        /// <returns>Value of a given property.</returns>
        public object this[string index]
        {
            get
            {
                ThrowIfDisposed();
                return GetValue(index);
            }
            set
            {
                ThrowIfDisposed();
                SetValue(index, value);
            }
        }

        #endregion

        #region Public GetValue Implementation

        /// <summary>
        /// Gets a value of a given property.
        /// </summary>
        /// <param name="property"><see cref="MetaProperty"/> which identifies the
        /// property a value is retrieved for.</param>
        /// <returns>Value of a given property.</returns>
        public object GetValue(MetaProperty property)
        {
            ThrowIfDisposed();

            if (property == null)
                throw new MetaPropertyException(string.Format(Res.ExceptionPropertyNotFound, "null"));

            return m_MetaPropertyManager.GetPropertyValue(property);
        }

        /// <summary>
        /// Gets a value of a given property.
        /// </summary>
        /// <param name="property">Property name which identifies the
        /// property a value is retrieved for.</param>
        /// <returns>Value of a given property.</returns>
        public object GetValue(string property)
        {
            ThrowIfDisposed();

            if (string.IsNullOrEmpty(property))
                throw new MetaPropertyException(Res.ExceptionPropertyNameNull);

            MetaProperty metaProperty = m_MetaPropertyManager.GetMetaProperty(property);

            if (metaProperty == null)
                throw new MetaPropertyException(string.Format(Res.ExceptionPropertyNotFound, property));

            return GetValue(metaProperty);
        }

        #endregion

        #region Public SetValue Implementation

        [NonSerialized]
        private SetterStrategy m_Setter = SetterStrategies.Standard;

        /// <summary>
        /// Gets or sets a setter strategy. This property cannot be null.
        /// </summary>
        public virtual SetterStrategy Setter
        {
            get { return m_Setter; }
            set
            {
                if (value == null)
                    throw new InvalidOperationException();

                m_Setter = value;
            }
        }

        /// <summary>
        /// Sets a value for a given property.
        /// </summary>
        /// <param name="property">Property name which identifies the
        /// property a value is set for.</param>
        /// <param name="value">The value being set.</param>
        public void SetValue(string property, object value)
        {
            ThrowIfDisposed();

            if (string.IsNullOrEmpty(property))
                throw new MetaPropertyException(Res.ExceptionPropertyNameNull);

            MetaProperty metaProperty = m_MetaPropertyManager.GetMetaProperty(property);

            if (metaProperty == null)
                throw new MetaPropertyException(string.Format(Res.ExceptionPropertyNotFound, property));

            SetValue(metaProperty, value);
        }

        /// <summary>
        /// Sets a value for a given property.
        /// </summary>
        /// <param name="metaProperty"><see cref="MetaProperty"/> which identifies the
        /// property a value is set for.</param>
        /// <param name="value">The value being set.</param>
        public void SetValue(MetaProperty metaProperty, object value)
        {
            ThrowIfDisposed();

            if (metaProperty == null)
                throw new MetaPropertyException(string.Format(Res.ExceptionPropertyNotFound, "null"));

            Setter(this, metaProperty, value, obj => m_MetaPropertyManager.SetPropertyValue(metaProperty, obj));
        }

        /// <summary>
        /// Sets a value for a given property.
        /// </summary>
        /// <param name="propertyKey"><see cref="MetaPropertyKey"/> which identifies the
        /// read-only property a value is set for.</param>
        /// <param name="value">The value being set.</param>
        public void SetValue(MetaPropertyKey propertyKey, object value)
        {
            ThrowIfDisposed();

            if (propertyKey == null)
                throw new MetaPropertyException(string.Format(Res.ExceptionPropertyNotFound, "null"));

            Setter(this, propertyKey.MetaProperty, value, obj => m_MetaPropertyManager.SetPropertyValue(propertyKey, obj));
        }

        /// <summary>
        /// Represents a setter strategy delegate.
        /// </summary>
        /// <param name="container"></param>
        /// <param name="metaProperty"></param>
        /// <param name="value"></param>
        /// <param name="setter"></param>
        public delegate void SetterStrategy(PropertyContainer container, MetaProperty metaProperty, object value, Action<object> setter);

        /// <summary>
        /// Contains predefined setter strategies.
        /// </summary>
        public static class SetterStrategies
        {
            /// <summary>
            /// Setter strategy which raises <see cref="PropertyContainer.PropertyChanging"/> event,
            /// <see cref="PropertyContainer.PropertyChanged"/> event, validates property and synchronizes
            /// property with other properties.
            /// </summary>
            public static readonly SetterStrategy Standard =
                (container, metaProperty, value, setter) =>
                {
                    var oldValue = container.m_MetaPropertyManager.GetProperty(metaProperty).Value;

                    if (!Equals(oldValue, value))
                    {
                        var metadataHierarchy = metaProperty.GetMetadataHierarchy(container.m_ThisType);
                        var metadata = metadataHierarchy.First();

                        // Invoke last registered CoerceValue callback
                        // and raise PropertyChanging event
                        container.OnPropertyChanging(metaProperty, metadata, oldValue, ref value);

                        if (!Equals(value, MetaProperty.UnsetValue))
                        {
                            // Validate property being set
                            var propertyIsValid = container.OnValidateProperty(metaProperty, value);

                            if (metadata.ContainsOption(PropertyOptions.ThrowsOnValidationError) && !propertyIsValid)
                                throw new ArgumentException(
                                    string.Format(Res.ExceptionGivenValueIsNotValid, metaProperty.Name));

                            // Set the value using meta property key
                            setter.Invoke(value);

                            // Update IsValid property
                            container.OnValidationStatusChanged();

                            // Invoke all registered PropertyChanged callbacks
                            // and raise PropertyChanged event
                            container.OnPropertyChanged(metaProperty, metadataHierarchy, oldValue, value);

                            // Handle association synchronization
                            container.OnAssociationSynchronization(metaProperty, metadataHierarchy, oldValue, value);
                        }
                    }
                };

            /// <summary>
            /// Setter strategy which only synchronizes
            /// property with other properties.
            /// </summary>
            public static readonly SetterStrategy Fast =
                (container, metaProperty, value, setter) =>
                {
                    var oldValue = container.m_MetaPropertyManager.GetProperty(metaProperty).Value;

                    if (!Equals(oldValue, value) &&
                        !Equals(value, MetaProperty.UnsetValue))
                    {
                        var metadataHierarchy = metaProperty.GetMetadataHierarchy(container.m_ThisType);

                        // Set the value using meta property key
                        setter.Invoke(value);

                        // Invoke all registered PropertyChanged callbacks
                        // and raise PropertyChanged event
                        container.OnPropertyChanged(metaProperty, metadataHierarchy, oldValue, value);

                        // Handle association synchronization
                        container.OnAssociationSynchronization(metaProperty, metadataHierarchy, oldValue, value);
                    }
                };

            /// <summary>
            /// Setter strategy which does nothing but sets the value.
            /// </summary>
            public static readonly SetterStrategy Instant =
                (container, metaProperty, value, setter) =>
                {
                    // Set the value using meta property key
                    setter.Invoke(value);
                };
        }

        #endregion

        #region Public ClearPropertyValue Implementation

        /// <summary>
        /// Clears the value for a given property.
        /// </summary>
        /// <param name="metaProperty"><see cref="MetaProperty"/> which identifies the
        /// property for which the value is being cleared.</param>
        public void ClearValue(MetaProperty metaProperty)
        {
            ThrowIfDisposed();

            if (metaProperty == null)
                throw new MetaPropertyException(string.Format(Res.ExceptionPropertyNotFound, "null"));
            
            // Attached property should be removed
            if (metaProperty.PropertyKind == MetaPropertyKind.Attached)
            {
                m_MetaPropertyManager.RemoveDynamicProperty(metaProperty);
                return;
            }
            
            // All other properties should have their default values set
            var metadata = metaProperty.GetMetadata(m_ThisType);
            var defaultValue = m_MetaPropertyManager.GetDefaultValue(metaProperty, metadata);
            
            SetValue(metaProperty, defaultValue);
        }

        /// <summary>
        /// Clears the value for a given property.
        /// </summary>
        /// <param name="property">Property name which identifies the
        /// property for which the value is being cleared.</param>
        public void ClearValue(string property)
        {
            ThrowIfDisposed();

            if (string.IsNullOrEmpty(property))
                throw new MetaPropertyException(Res.ExceptionPropertyNameNull);

            MetaProperty metaProperty = m_MetaPropertyManager.GetMetaProperty(property);

            if (metaProperty == null)
                throw new MetaPropertyException(string.Format(Res.ExceptionPropertyNotFound, property));

            ClearValue(metaProperty);
        }

        #endregion

        #region Public HasDefaultValue Implementation

        /// <summary>
        /// Method used for checking if a given property has a default value.
        /// </summary>
        /// <param name="metaProperty"><see cref="MetaProperty"/> identifying
        /// property to check.</param>
        /// <returns>True if given property has a default value set. False otherwise.</returns>
        public bool HasDefaultValue(MetaProperty metaProperty)
        {
            ThrowIfDisposed();

            if (metaProperty == null)
                throw new MetaPropertyException(string.Format(Res.ExceptionPropertyNotFound, "null"));

            PropertyMetadata metadata = metaProperty.GetMetadata(m_ThisType);
            object currentValue = GetValue(metaProperty);
            object defaultValue = m_MetaPropertyManager.GetDefaultValue(metaProperty, metadata);

            return Equals(currentValue, defaultValue);
        }

        /// <summary>
        /// Method used for checking if a given property has a default value.
        /// </summary>
        /// <param name="propertyName">Property name identifying
        /// property to check.</param>
        /// <returns>True if given property has a default value set. False otherwise.</returns>
        public bool HasDefaultValue(string propertyName)
        {
            ThrowIfDisposed();

            if (string.IsNullOrEmpty(propertyName))
                throw new MetaPropertyException(Res.ExceptionPropertyNameNull);

            MetaProperty metaProperty = m_MetaPropertyManager.GetMetaProperty(propertyName);

            if (metaProperty == null)
                throw new MetaPropertyException(string.Format(Res.ExceptionPropertyNotFound, propertyName));

            return HasDefaultValue(metaProperty);
        }

        #endregion

        #region Public HasValidValue Implementation

        /// <summary>
        /// Checks if the property has a valid value.
        /// </summary>
        /// <param name="property">Name of the property to be checked.</param>
        /// <returns>True if the property has a valid value. False otherwise.</returns>
        public bool HasValidValue(string property)
        {
            MetaProperty metaProperty = GetProperty(property);
            if (metaProperty == null)
                throw new MetaPropertyException(string.Format(Res.ExceptionPropertyNotFound, property));

            return HasValidValue(metaProperty);
        }

        /// <summary>
        /// Checks if the property has a valid value.
        /// </summary>
        /// <param name="metaProperty"><see cref="MetaProperty"/> instance which identifies a property.</param>
        /// <returns>True if the property has a valid value. False otherwise.</returns>
        public bool HasValidValue(MetaProperty metaProperty)
        {
            if (metaProperty == null)
                throw new MetaPropertyException(string.Format(Res.ExceptionPropertyNotFound, "null"));

            var property = m_MetaPropertyManager.GetProperty(metaProperty);
            if (property == null)
                throw new MetaPropertyException(string.Format(Res.ExceptionPropertyNotFound, metaProperty.Name));

            return m_ValidationResults.CheckPropertyValidation(metaProperty);
        }

        #endregion

        #region Public Property Registration

        /// <summary>
        /// Registers new dynamic property. Dynamic property is usually added per object instance.
        /// </summary>
        /// <param name="name">Property name. Must be unique in current context.</param>
        /// <param name="propertyType">CLR property type.</param>
        /// <returns>Newly created <see cref="MetaProperty"/>.</returns>
        public MetaProperty RegisterDynamic(string name, Type propertyType)
        {
            ThrowIfDisposed();

            if (string.IsNullOrEmpty(name))
                throw new MetaPropertyException(Res.ExceptionPropertyNameNull);

            MetaProperty metaProperty = PropertyManager.RegisterDynamic(name, propertyType, m_ThisType);
            m_MetaPropertyManager.AddDynamicProperty(metaProperty);
            metaProperty.ValidationRules.RuleListChanged += OnRuleListChangedHandler;

            return metaProperty;
        }

        /// <summary>
        /// Registers new dynamic property. Dynamic property is usually added per object instance.
        /// </summary>
        /// <param name="name">Property name. Must be unique in current context.</param>
        /// <param name="propertyType">CLR property type.</param>
        /// <param name="metadata">Property metadata.</param>
        /// <returns>Newly created <see cref="MetaProperty"/>.</returns>
        public MetaProperty RegisterDynamic(string name, Type propertyType, PropertyMetadata metadata)
        {
            ThrowIfDisposed();

            if (string.IsNullOrEmpty(name))
                throw new MetaPropertyException(Res.ExceptionPropertyNameNull);

            MetaProperty metaProperty = PropertyManager.RegisterDynamic(name, propertyType, m_ThisType, metadata);
            m_MetaPropertyManager.AddDynamicProperty(metaProperty);
            metaProperty.ValidationRules.RuleListChanged += OnRuleListChangedHandler;

            return metaProperty;
        }

        /// <summary>
        /// Unregisters dynamic property.
        /// </summary>
        /// <param name="name">Name of property to be removed. If property hasn't been previously registered, exception is thrown.</param>
        public void UnregisterDynamic(string name)
        {
            ThrowIfDisposed();

            if (string.IsNullOrEmpty(name))
                throw new MetaPropertyException(Res.ExceptionPropertyNameNull);

            MetaProperty metaProperty = m_MetaPropertyManager.GetMetaProperty(name);

            if (metaProperty == null)
                throw new MetaPropertyException(string.Format(Res.ExceptionPropertyNotFound, name));

            UnregisterDynamic(metaProperty);
        }

        /// <summary>
        /// Unregisters dynamic property. 
        /// </summary>
        /// <param name="metaProperty">Property to be removed. If property hasn't been previously registered, exception is thrown.</param>
        public void UnregisterDynamic(MetaProperty metaProperty)
        {
            ThrowIfDisposed();

            if (metaProperty == null)
                throw new MetaPropertyException(string.Format(Res.ExceptionPropertyNotFound, "null"));

            m_MetaPropertyManager.RemoveDynamicProperty(metaProperty);
            metaProperty.ValidationRules.RuleListChanged -= OnRuleListChangedHandler;
        }

        private void OnRuleListChangedHandler(object sender, RuleListChangedEventArgs e)
        {
            foreach (var metaProperty in GetDynamicProperties())
            {
                var property = m_MetaPropertyManager.GetProperty(metaProperty);
                OnValidateProperty(metaProperty, property.Value);
            }

            // Update IsValid property
            OnValidationStatusChanged();
        }

        #endregion

        #region Public MetaProperty Getters

        /// <summary>
        /// Returns a collection of all meta properties registered for this instance.
        /// </summary>
        /// <returns>Collection of all meta properties registered for this instance.</returns>
        public MetaPropertyCollection GetProperties()
        {
            ThrowIfDisposed();

            return m_MetaPropertyManager.GetMetaProperties();
        }

        /// <summary>
        /// Method that returns all dynamic properties registered in the registry.
        /// </summary>
        /// <returns>Returns all dynamic properties registered in the registry.</returns>
        public MetaPropertyCollection GetDynamicProperties()
        {
            ThrowIfDisposed();

            return m_MetaPropertyManager.GetDynamicMetaProperties();
        }

        /// <summary>
        /// Gets a meta property for a property specified by name, or null, if property does not exist.
        /// </summary>
        /// <param name="name">Name of the property.</param>
        /// <returns>Meta property for a property specified by name, or null, if property does not exist.</returns>
        public MetaProperty GetProperty(string name)
        {
            ThrowIfDisposed();

            if (string.IsNullOrEmpty(name))
                throw new MetaPropertyException(Res.ExceptionPropertyNameNull);

            return m_MetaPropertyManager.GetMetaProperty(name);
        }

        #endregion

        #region Public Scope Management

        /// <summary>
        /// Adds this property container to a specified scope.
        /// </summary>
        /// <param name="scope">Scope this object is being added to.</param>
        /// <returns>Scope this object is being added to.</returns>
        public IPropertyScope AddScope(IPropertyScope scope)
        {
            ThrowIfDisposed();

            if (scope == null)
                throw new ArgumentNullException("scope", Res.ExceptionScopeIsNull);

            m_MetaPropertyManager.AddPropertyScope(scope);
            return scope;
        }

        /// <summary>
        /// Removes this property container from a specified scope.
        /// </summary>
        /// <param name="scope">Scope this object is being removed from.</param>
        public void RemoveScope(IPropertyScope scope)
        {
            ThrowIfDisposed();

            if (scope == null)
                throw new ArgumentNullException("scope", Res.ExceptionScopeIsNull);

            m_MetaPropertyManager.RemovePropertyScope(scope);
        }

        #endregion

        #region Public Content Equality Implementation

        /// <summary>
        /// Compares content of two <see cref="IPropertyContainer"/> instances.
        /// </summary>
        /// <param name="compareTarget">Target which this instance is compared to.</param>
        /// <returns>True if instances contain the same matchable content.</returns>
        /// <remarks>
        /// This method uses only properties which are designated for matching.
        /// Both value and reference types are compared using Equals method rather than
        /// ContentEquals. You should avoid redirecting Equals method to the ContentEquals
        /// method as this might lead to circular comparisons.
        /// </remarks>
        public virtual bool ContentEquals(IPropertyContainer compareTarget)
        {
            ThrowIfDisposed();

            if (ReferenceEquals(null, compareTarget)) return false;
            if (ReferenceEquals(this, compareTarget)) return true;
            if (m_ThisType != compareTarget.GetType()) return false;

            MetaPropertyCollection properties = GetProperties();
            MetaPropertyCollection targetProperties = compareTarget.GetProperties();
            
            IDictionary<string, MetaProperty> propertyMap = new Dictionary<string, MetaProperty>();
            IDictionary<string, MetaProperty> targetPropertyMap = new Dictionary<string, MetaProperty>();

            foreach (var property in properties)
            {
                PropertyMetadata propertyMetadata = property.GetMetadata(m_ThisType);
                if (propertyMetadata.ContainsOption(PropertyOptions.Matchable))
                    propertyMap.Add(property.Name, property);
            }

            foreach (var property in targetProperties)
            {
                PropertyMetadata propertyMetadata = property.GetMetadata(m_ThisType);
                if (propertyMetadata.ContainsOption(PropertyOptions.Matchable))
                    targetPropertyMap.Add(property.Name, property);
            }

            if (propertyMap.Keys.Count != targetPropertyMap.Keys.Count) return false;

            return DoContentEquals(compareTarget, propertyMap, targetPropertyMap);
        }

        /// <summary>
        /// Compares content of two <see cref="IPropertyContainer"/> instances.
        /// </summary>
        /// <param name="compareTarget">Target which this instance is compared to.</param>
        /// <param name="propertyNames">Properties to be used for comparison. Bear in mind that
        /// given parameters don't have to be marked as matchable.</param>
        /// <returns>True if instances contain the same matchable content.</returns>
        /// <remarks>
        /// This method uses only properties which are designated for matching.
        /// Both value and reference types are compared using Equals method rather than
        /// ContentEquals. You should avoid redirecting Equals method to the ContentEquals
        /// method as this might lead to circular comparisons.
        /// </remarks>
        public virtual bool ContentEquals(IPropertyContainer compareTarget, params string[] propertyNames)
        {
            ThrowIfDisposed();

            if (ReferenceEquals(null, compareTarget)) return false;
            if (ReferenceEquals(this, compareTarget)) return true;
            if (m_ThisType != compareTarget.GetType()) return false;

            IDictionary<string, MetaProperty> propertyMap = new Dictionary<string, MetaProperty>();
            IDictionary<string, MetaProperty> targetPropertyMap = new Dictionary<string, MetaProperty>();

            foreach (var propertyName in propertyNames)
            {
                MetaProperty property = GetProperty(propertyName);
                if (property == null) return false;

                MetaProperty targetProperty = compareTarget.GetProperty(propertyName);
                if (targetProperty == null) return false;
                
                propertyMap.Add(property.Name, property);
                targetPropertyMap.Add(targetProperty.Name, targetProperty);
            }

            return DoContentEquals(compareTarget, propertyMap, targetPropertyMap);
        }

        /// <summary>
        /// Compares content of two <see cref="IPropertyContainer"/> instances.
        /// </summary>
        /// <param name="compareTarget">Target which this instance is compared to.</param>
        /// <param name="propertyNames">Properties to be used for comparison. Bear in mind that
        /// given parameters don't have to be marked as matchable.</param>
        /// <returns>True if instances contain the same matchable content.</returns>
        /// <remarks>
        /// This method uses only properties which are designated for matching.
        /// Both value and reference types are compared using Equals method rather than
        /// ContentEquals. You should avoid redirecting Equals method to the ContentEquals
        /// method as this might lead to circular comparisons.
        /// </remarks>
        public bool ContentEquals(IPropertyContainer compareTarget, IEnumerable<string> propertyNames)
        {
            ThrowIfDisposed();

            return ContentEquals(compareTarget, new List<string>(propertyNames).ToArray());
        }

        private bool DoContentEquals(IPropertyContainer compareTarget,
            IDictionary<string, MetaProperty> sourceProperties,
            IDictionary<string, MetaProperty> targetProperties)
        {
            Debug.Assert(sourceProperties.Keys.Count == targetProperties.Keys.Count);

            foreach (var propertyName in sourceProperties.Keys)
            {
                MetaProperty property = sourceProperties[propertyName];
                MetaProperty targetProperty;
                bool contains = targetProperties.TryGetValue(propertyName, out targetProperty);

                if (!contains ||
                    property.PropertyType != targetProperty.PropertyType) return false;

                var sourceValue = GetValue(property);
                var targetValue = compareTarget.GetValue(targetProperty);

                if (sourceValue is IContentComparable)
                {
                    Debug.Assert(targetValue is IContentComparable);

                    var sourceValueContentComparable = (IContentComparable) sourceValue;
                    if (sourceValueContentComparable.CompareContentTo(targetValue) != 0) return false;
                }
                else if (!Equals(sourceValue, targetValue)) return false;
            }

            return true;
        }

        #endregion

        #region Public GetDuck Implementation

        private static readonly ProxyGenerator Generator = new ProxyGenerator();

        /// <summary>
        /// Returns a proxied interface which defines properties present
        /// on the <see cref="PropertyContainer"/>.
        /// </summary>
        /// <typeparam name="TInterface">Type of the interface to be returned.</typeparam>
        /// <returns>proxied interface which defines properties present
        /// on the <see cref="PropertyContainer"/>, or null if the interface
        /// cannot be satisfied.</returns>
        public TInterface GetDuck<TInterface>() where TInterface : class
        {
            if (!CanCreateDuck(typeof(TInterface)))
                return null;

            var properties = GetPropertiesUsingReflection(typeof(TInterface));
            var propertyMap = new Dictionary<string, MetaProperty>();

            foreach (var reflectionPropertyInfo in properties)
            {
                if (reflectionPropertyInfo.Name == "PropertyContainer" &&
                    reflectionPropertyInfo.PropertyType == typeof(IPropertyContainer))
                    continue;

                MetaProperty metaProperty = GetProperty(reflectionPropertyInfo.Name);

                if (metaProperty == null)
                    return null;

                if (metaProperty.PropertyType != reflectionPropertyInfo.PropertyType)
                    return null;

                if (metaProperty.IsReadOnly && reflectionPropertyInfo.CanWrite)
                    return null;

                propertyMap.Add(metaProperty.Name, metaProperty);
            }

            return Generator.CreateInterfaceProxyWithoutTarget<TInterface>(
                new PropertyInterceptor(this, propertyMap));
        }

        private static bool CanCreateDuck(Type type)
        {
            if (type.IsInterface == false)
                return false;

            var methods = type.GetMethods();
            foreach (var methodInfo in methods)
            {
                if (!methodInfo.Name.StartsWith("get_") &&
                    !methodInfo.Name.StartsWith("set_"))
                    return false;
            }

            return true;
        }

        private static System.Reflection.PropertyInfo[] GetPropertiesUsingReflection(Type type)
        {
            return type.GetProperties();
        }

        private class PropertyInterceptor : IInterceptor
        {
            private readonly IPropertyContainer m_PropertyContainer;
            private readonly Dictionary<string, MetaProperty> m_PropertyMap;

            public PropertyInterceptor(IPropertyContainer propertyContainer,
                                       Dictionary<string, MetaProperty> propertyMap)
            {
                Debug.Assert(propertyContainer != null);
                Debug.Assert(propertyMap != null);

                m_PropertyContainer = propertyContainer;
                m_PropertyMap = propertyMap;
            }

            public void Intercept(IInvocation invocation)
            {
                if (invocation.Method.Name == "get_PropertyContainer")
                {
                    invocation.ReturnValue = m_PropertyContainer;
                }
                else if (invocation.Method.Name.StartsWith("set_"))
                {
                    string name = invocation.Method.Name.Substring(4, invocation.Method.Name.Length - 4);
                    MetaProperty metaProperty = m_PropertyMap[name];

                    m_PropertyContainer.SetValue(metaProperty, invocation.Arguments[0]);
                }
                else if (invocation.Method.Name.StartsWith("get_"))
                {
                    string name = invocation.Method.Name.Substring(4, invocation.Method.Name.Length - 4);
                    MetaProperty metaProperty = m_PropertyMap[name];

                    object val = m_PropertyContainer.GetValue(metaProperty);
                    invocation.ReturnValue = val;
                }
                else
                {
                    throw new PropertyContainerException(Res.ExceptionProxyInvalidState);
                }
            }
        }

        #endregion

        #region Protected Validation

        [NonSerialized]
        private bool m_IsValid = true;

        /// <summary>
        /// Gets a value indicating whether object is in valid state. This means no 
        /// validation rules are broken.
        /// </summary>
        public bool IsValid
        {
            get { return m_IsValid; }
            private set
            {
                if (value != IsValid)
                {
                    OnPropertyChanging<PropertyContainer>(t => t.IsValid);

                    m_IsValid = value;

                    OnPropertyChanged<PropertyContainer>(t => t.IsValid);
                }
            }
        }

        /// <summary>
        /// Contains an add-only list of object validators.
        /// </summary>
        internal IObjectRuleList ObjectValidators
        {
            get { return m_ObjectRuleList; }
        }

        /// <summary>
        /// Validates given property and updates <see cref="PropertyContainer"/>'s validation status.
        /// </summary>
        /// <param name="metaProperty">Property to be validated.</param>
        /// <remarks>You can use this method to manually trigger property validation whenever
        /// property's value changes.</remarks>
        public void ValidateProperty(MetaProperty metaProperty)
        {
            var value = GetValue(metaProperty);
            OnValidateProperty(metaProperty, value);

            // Update IsValid property
            OnValidationStatusChanged();
        }

        /// <summary>
        /// Method called whenever validation status changes and needs to be updated.
        /// </summary>
        protected virtual void OnValidationStatusChanged()
        {
            var propertiesAreValid = true;
            var objectIsValid = OnValidateObject();

            foreach (var property in m_MetaPropertyManager.GetProperties())
            {
                if (m_ValidationResults.CheckPropertyValidation(property.MetaProperty) == false)
                {
                    propertiesAreValid = false;
                    break;
                }
            }

            IsValid = propertiesAreValid && objectIsValid;
        }

        private bool ValidateProperty(IProperty property)
        {
            if (property.MetaProperty.IsValidatable == false)
            {
                m_ValidationResults.ClearPropertyValidation(property.MetaProperty);
                return true;
            }

            return ValidateProperty(property, property.Value);
        }

        private bool ValidateProperty(IProperty property, object value)
        {
            m_ValidationResults.ClearPropertyValidation(property.MetaProperty);

            if (property.MetaProperty.IsValidatable == false)
            {
                return true;
            }

            var isValid = true;
            var args = new ValidationArgs(this, property.MetaProperty);

            foreach (var rule in property.MetaProperty.ValidationRules)
            {
                var result = rule.Validate(value, args);
                if (result.IsValid == false)
                {
                    isValid = false;
                    m_ValidationResults.AddValidationResult(property.MetaProperty, result);
                }
            }

            foreach (var rule in ValidationManager.GetRuleList(property.MetaProperty.PropertyType))
            {
                var result = rule.Validate(value, args);
                if (result.IsValid == false)
                {
                    isValid = false;
                    m_ValidationResults.AddValidationResult(property.MetaProperty, result);
                }
            }

            return isValid;
        }

        /// <summary>
        /// Mathod called when a property needs to be validated. Does not update <see cref="IPropertyContainer.IsValid"/> property.
        /// </summary>
        /// <param name="metaProperty"><see cref="MetaProperty"/> identifying a property to be validated.</param>
        /// <param name="value">Value to be validated.</param>
        /// <returns>True if given value is valid. False otherwise.</returns>
        protected virtual bool OnValidateProperty(MetaProperty metaProperty, object value)
        {
            var property = m_MetaPropertyManager.GetProperty(metaProperty);
            return ValidateProperty(property, value);
        }

        /// <summary>
        /// Method called when whole object needs to be validated.
        /// </summary>
        /// <returns>True if the object is valid. False otherwise.</returns>
        protected virtual bool OnValidateObject()
        {
            m_ValidationResults.ClearObjectValidation();

            var isValid = true;

            foreach (var rule in m_ObjectRuleList)
            {
                var result = rule.Validate(this);
                if (result.IsValid == false)
                {
                    isValid = false;
                    m_ValidationResults.AddValidationResult(result);
                }
            }

            return isValid;
        }

        /// <summary>
        /// Method called when wall properties need to be validated.
        /// </summary>
        /// <returns>True if the all properties are valid. False otherwise.</returns>
        protected virtual bool OnValidateProperties()
        {
            bool propertiesAreValid = true;

            foreach (var property in m_MetaPropertyManager.GetProperties())
            {
                // Can't put break here since all properties have to validated
                if (ValidateProperty(property) == false)
                    propertiesAreValid = false;
            }

            return propertiesAreValid;
        }

        /// <summary>
        /// Performs full property and object validation and updates <see cref="IsValid"/> property.
        /// </summary>
        /// <remarks>
        /// This method is used during object creation, deserialization and
        /// when feature suppression is off.
        /// </remarks>
        public virtual void Validate()
        {
            bool objectIsValid = OnValidateObject();
            bool propertiesAreValid = OnValidateProperties();

            IsValid = propertiesAreValid && objectIsValid;
        }

        /// <summary>
        /// Method called in the object's constructor befor initial validation takes place.
        /// </summary>
        protected virtual void OnAddObjectValidators()
        {
        }

        #endregion

        #region Protected Extensibility Methods

        protected virtual IMetaPropertyManager GetMetaPropertyManagerOverride(IPropertyContainer parent)
        {
            return new MetaPropertyManager(parent);
        }

        protected virtual IObjectRuleList GetObjectRuleListOverride()
        {
            return new ObjectRuleList();
        }

        /// <summary>
        /// Initialization method called after <see cref="IPropertyContainer"/>
        /// has been initialized but before validation takes place.
        /// </summary>
        protected virtual void OnInitialize()
        {
        }

        #endregion

        #region Protected Change Notification

        protected virtual void OnPropertyChanging(MetaProperty metaProperty, PropertyMetadata currentMetadata, object oldValue, ref object newValue)
        {
            // Invoke coerce callback only from the most-derieved class
            if (currentMetadata.CoerceValue != null)
                newValue = currentMetadata.CoerceValue(this, newValue);

            OnPropertyChanging(metaProperty.Name);
        }

        protected virtual void OnPropertyChanged(MetaProperty metaProperty, IEnumerable<PropertyMetadata> metadataHierarchy, object oldValue, object newValue)
        {
            var e = new PropertyChangedEventArgs(oldValue, newValue, metaProperty);

            // Invoke handlers from base class to the derived class direction
            foreach (var metadata in metadataHierarchy.Reverse())
            {
                if (metadata.PropertyChanged != null)
                    metadata.PropertyChanged(this, e);
            }

            OnPropertyChanged(metaProperty.Name);

            if (metaProperty.DependentTransientProperties != null)
            {
                // Notify all dependent properties
                foreach (var dependentProperty in metaProperty.DependentTransientProperties)
                {
                    OnPropertyChanged(dependentProperty, dependentProperty.GetMetadataHierarchy(m_ThisType),
                        MetaProperty.UnknownValue, MetaProperty.UnknownValue);
                }
            }
        }

        #endregion

        #region Protected Association Synchronization

        protected virtual void OnAssociationResynchronization()
        {
            foreach (var property in GetProperties())
            {
                var metadataHierarchy = property.GetMetadataHierarchy(m_ThisType);
                var currentValue = GetValue(property);

                foreach (var metadata in metadataHierarchy.Reverse())
                {
                    if (metadata.AssociationSync != null)
                        HandleAssociationResynchronization(metadata.AssociationSync, property, currentValue);
                }
            }
        }

        protected virtual void OnAssociationUnsynchronization()
        {
            foreach (var property in GetProperties())
            {
                var metadataHierarchy = property.GetMetadataHierarchy(m_ThisType);
                var currentValue = GetValue(property);

                foreach (var metadata in metadataHierarchy.Reverse())
                {
                    if (metadata.AssociationSync != null)
                        HandleAssociationUnsynchronization(metadata.AssociationSync, property, currentValue);
                }
            }
        }

        protected virtual void OnAssociationSynchronization(MetaProperty property, IEnumerable<PropertyMetadata> metadataHierarchy, object oldValue, object newValue)
        {
            // Invoke handlers from base class to the derived class direction
            foreach (var metadata in metadataHierarchy.Reverse())
            {
                if (metadata.AssociationSync != null)
                    HandleAssociationSynchronization(metadata.AssociationSync, property, oldValue, newValue);
            }
        }

        #endregion

        #region IEditableObject Implementation

        [NonSerialized]
        private bool m_IsEditing;

        [NonSerialized]
        private IPropertyContainer m_Copy;

        /// <summary>
        /// Gets a value indicating whether the object is being edited.
        /// </summary>
        public bool IsEditing
        {
            get { return m_IsEditing; }
            private set
            {
                if (m_IsEditing != value)
                {
                    m_IsEditing = value;
                    OnPropertyChanged<PropertyContainer>(t => IsEditing);
                }
            }
        }

        /// <summary>
        /// Creates a new instance of current class for <see cref="IEditableObject"/> functionality.
        /// </summary>
        /// <returns></returns>
        protected virtual IPropertyContainer CreateInstance()
        {
            return (IPropertyContainer)Activator.CreateInstance(m_ThisType);
        }

        protected virtual void OnBeginEdit()
        {
            m_Copy = CreateInstance();
            PropertyMapper.CopyProperties(this, m_Copy);
        }

        protected virtual void OnEndEdit()
        {
            m_Copy = null;
        }

        protected virtual void OnCancelEdit()
        {
            PropertyMapper.RestoreProperties(m_Copy, this);
            m_Copy = null;
        }

        void IEditableObject.BeginEdit()
        {
            if (!IsEditing)
            {
                IsEditing = true;
                OnBeginEdit();
            }
        }

        void IEditableObject.EndEdit()
        {
            if (IsEditing)
            {
                OnEndEdit();
                IsEditing = false;
            }
        }

        void IEditableObject.CancelEdit()
        {
            if (IsEditing)
            {
                OnCancelEdit();
                IsEditing = false;
            }
        }

        #endregion

        #region INotifyObjectStateChanged Implementation

        /// <summary>
        /// Backend event property for <see cref="ObjectStateChanged"/> event.
        /// </summary>
        [field:NonSerialized]
        private event EventHandler<EventArgs> ObjectStateChangedEvent;

        /// <summary>
        /// Event raised whenever <see cref="IValidatable.IsValid"/> property changes.
        /// </summary>
        event EventHandler<EventArgs> INotifyObjectStateChanged.ObjectStateChanged
        {
            add { ObjectStateChangedEvent += value; }
            remove { ObjectStateChangedEvent -= value; }
        }

        private void OnObjectStateChanged()
        {
            if (ObjectStateChangedEvent != null)
                ObjectStateChangedEvent(this, EventArgs.Empty);
        }

        #endregion

        #region INotifyPropertyChanging Implementation

        [field:NonSerialized]
        private event PropertyChangingEventHandler PropertyChangingEvent;

        event PropertyChangingEventHandler INotifyPropertyChanging.PropertyChanging
        { 
            add { PropertyChangingEvent += value; }
            remove { PropertyChangingEvent -= value; }
        }

        private void OnPropertyChanging(string property)
        {
            if (PropertyChangingEvent != null)
                PropertyChangingEvent(this, new PropertyChangingEventArgs(property));
        }

        protected void OnPropertyChanging<T>(Expression<Func<T, object>> property)
        {
            OnPropertyChanging(typeof(T).GetPropertyName(property));
        }

        #endregion

        #region INotifyPropertyChanged Implementation

        [field:NonSerialized]
        private event PropertyChangedEventHandler PropertyChangedEvent;

        event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged
        {
            add { PropertyChangedEvent += value; }
            remove { PropertyChangedEvent -= value; }
        }

        private void OnPropertyChanged(string property)
        {
            if (PropertyChangedEvent != null)
                PropertyChangedEvent(this, new System.ComponentModel.PropertyChangedEventArgs(property));

            OnObjectStateChanged();
        }

        protected void OnPropertyChanged<T>(Expression<Func<T, object>> property)
        {
            OnPropertyChanged(typeof(T).GetPropertyName(property));
        }

        #endregion

        #region IDataErrorInfo Implementation

        string IDataErrorInfo.this[string columnName]
        {
            get
            {
                MetaProperty metaProperty = GetProperty(columnName);
                if (metaProperty == null)
                    throw new MetaPropertyException(string.Format(Res.ExceptionPropertyNotFound, columnName));

                var sb = new StringBuilder();
                
                foreach (var validationResult in m_ValidationResults.GetValidationResults(metaProperty))
                {
                    sb.AppendLine(validationResult.Error);
                }

                return sb.ToString();
            }
        }

        string IDataErrorInfo.Error
        {
            get
            {
                var sb = new StringBuilder();
                var properties = GetProperties();
                
                foreach (var property in properties)
                {
                    foreach (var validationResult in m_ValidationResults.GetValidationResults(property))
                    {
                        sb.AppendLine(validationResult.Error);
                    }
                }

                foreach (var validationResult in m_ValidationResults.GetValidationResults())
                {
                    sb.AppendLine(validationResult.Error);
                }

                return sb.ToString();
            }
        }

        #endregion

        #region IDisposable Implementation

        [NonSerialized]
        private bool m_Disposed;

        /// <summary>
        /// Returns true if object has been disposed.
        /// </summary>
        protected bool Disposed
        {
            get
            {
                lock (this)
                {
                    return m_Disposed;
                }
            }
        }

        /// <summary>
        /// Performs cleaning. Always call base class's <see cref="Dispose(bool)"/> method.
        /// </summary>
        /// <param name="disposing">True is this method is called via <see cref="Dispose"/> method.
        /// False indicates that the method has been called from destructor.</param>
        /// <remarks>
        /// Generally, you should dispose both managed and unmanaged resources when <paramref name="disposing"/>
        /// is true, and only unmanaged resources when <paramref name="disposing"/> is false.
        /// </remarks>
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                var disposableMetaPropertyManager = m_MetaPropertyManager as IDisposable;
                if (disposableMetaPropertyManager != null)
                {
                    disposableMetaPropertyManager.Dispose();
                }
            }
        }

        public void Dispose()
        {
            lock (this)
            {
                if (m_Disposed == false)
                {
                    Dispose(true);
                    m_Disposed = true;

                    GC.SuppressFinalize(this);
                }
            }
        }

        /// <summary>
        /// Destructor will run only if <see cref="Dispose"/> wasn't called.
        /// Do not provide descructors in derived types. Use <see cref="Dispose(bool)"/> instead.
        /// </summary>
        ~PropertyContainer()
        {
            Dispose(false);
        }

        [DebuggerStepThrough]
        private void ThrowIfDisposed()
        {
            if (Disposed)
                throw new ObjectDisposedException(GetType().Name);
        }

        #endregion

        #region Private Serialization Events

        [OnDeserialized]
        private void OnDeserialized(StreamingContext context)
        {
            m_ValidationResults = new ValidationResultCollection();

            if (typeof(ISerializable).IsAssignableFrom(GetType()) == false)
            {
                Validate();

                foreach (var metaProperty in GetDynamicProperties())
                {
                    metaProperty.ValidationRules.RuleListChanged += OnRuleListChangedHandler;
                }
            }

            m_Setter = SetterStrategies.Standard;
        }

        [OnSerializing]
        private void OnSerializing(StreamingContext context)
        {
            foreach (var property in m_MetaPropertyManager.GetProperties())
            {
                var metadata = property.MetaProperty.GetMetadata(m_ThisType);
                if (metadata.ContainsOption(PropertyOptions.NonSerializable))
                {
                    property.Value = null;
                }
            }
        }

        #endregion

        #region Private Association Synchronization

        private void HandleAssociationUnsynchronization(AssociationSync sync, MetaProperty property, object value)
        {
            Debug.Assert(sync != null);

            switch (sync.SyncType)
            {
                case SyncType.ManyToOne:

                    // Do nothing
                    break;

                case SyncType.OneToMany:

                    if (value != null)
                    {
                        NotifyCollectionChangedEventHandler oneToManyHandler = (sender, e) =>
                            OneToManyAssocSync.UpdateManySide(this, property, sync.MetaProperty, e);

                        var collection = (INotifyCollectionChanged)value;
                        collection.CollectionChanged -= oneToManyHandler;
                    }

                    break;

                case SyncType.ManyToMany:

                    if (value != null)
                    {
                        NotifyCollectionChangedEventHandler manyToManyHandler = (sender, e) =>
                            ManyToManyAssocSync.UpdateOtherSide(this, property, sync.MetaProperty, e);

                        var collection = (INotifyCollectionChanged)value;
                        collection.CollectionChanged -= manyToManyHandler;
                    }

                    break;

                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        private void HandleAssociationResynchronization(AssociationSync sync, MetaProperty property, object value)
        {
            Debug.Assert(sync != null);

            switch (sync.SyncType)
            {
                case SyncType.ManyToOne:

                    ManyToOneAssocSync.ResynchronizeOneSide(this, property, sync.MetaProperty, (PropertyContainer)value);
                    break;

                case SyncType.OneToMany:

                    OneToManyAssocSync.ResynchronizeManySide(this, property, sync.MetaProperty, value);

                    if (value != null)
                    {
                        NotifyCollectionChangedEventHandler oneToManyHandler = (sender, e) =>
                            OneToManyAssocSync.UpdateManySide(this, property, sync.MetaProperty, e);

                        var collection = (INotifyCollectionChanged)value;
                        collection.CollectionChanged += oneToManyHandler;
                    }

                    break;

                case SyncType.ManyToMany:

                    ManyToManyAssocSync.ResynchronizeOtherSide(this, property, sync.MetaProperty, value);
                    
                    if (value != null)
                    {
                        NotifyCollectionChangedEventHandler manyToManyHandler = (sender, e) =>
                            ManyToManyAssocSync.UpdateOtherSide(this, property, sync.MetaProperty, e);

                        var collection = (INotifyCollectionChanged)value;
                        collection.CollectionChanged += manyToManyHandler;
                    }
                    break;

                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        private void HandleAssociationSynchronization(AssociationSync sync, MetaProperty property, object oldValue, object newValue)
        {
            Debug.Assert(sync != null);
            sync.ValidateAssociation(oldValue, newValue);

            switch (sync.SyncType)
            {
                case SyncType.ManyToOne:

                    ManyToOneAssocSync.UpdateOneSide(this, property, sync.MetaProperty,
                        (PropertyContainer)oldValue, (PropertyContainer)newValue);

                    break;

                case SyncType.OneToMany:

                    NotifyCollectionChangedEventHandler oneToManyHandler = (sender, e) =>
                        OneToManyAssocSync.UpdateManySide(this, property, sync.MetaProperty, e);
                    
                    if (oldValue != null)
                    {
                        var oldCollection = (INotifyCollectionChanged)oldValue;
                        oldCollection.CollectionChanged -= oneToManyHandler;
                        OneToManyAssocSync.UnsynchronizeManySide(this, property, sync.MetaProperty, oldValue);
                    }
                    
                    if (newValue != null)
                    {
                        var newCollection = (INotifyCollectionChanged)newValue;
                        newCollection.CollectionChanged += oneToManyHandler;
                        OneToManyAssocSync.ResynchronizeManySide(this, property, sync.MetaProperty, newValue);
                    }
                    
                    break;

                case SyncType.ManyToMany:

                    NotifyCollectionChangedEventHandler manyToManyHandler = (sender, e) =>
                        ManyToManyAssocSync.UpdateOtherSide(this, property, sync.MetaProperty, e);

                    if (oldValue != null)
                    {
                        var oldCollection = (INotifyCollectionChanged)oldValue;
                        oldCollection.CollectionChanged -= manyToManyHandler;
                    }
                    
                    if (newValue != null)
                    {
                        var newCollection = (INotifyCollectionChanged)newValue;
                        newCollection.CollectionChanged += manyToManyHandler;
                    }
                    
                    break;

                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        internal bool IsSynchronizing(MetaProperty metaProperty)
        {
            var synchronizable = (ISynchronizable) m_MetaPropertyManager.GetProperty(metaProperty);
            return synchronizable.IsSynchronizing;
        }

        internal IDisposable BlockSynchronizationReentrency(MetaProperty metaProperty)
        {
            return new BlockingObject(m_MetaPropertyManager, metaProperty);
        }

        private class BlockingObject : IDisposable
        {
            private readonly IMetaPropertyManager m_MetaPropertyManager;
            private readonly MetaProperty m_MetaProperty;

            public BlockingObject(IMetaPropertyManager metaPropertyManager, MetaProperty metaProperty)
            {
                m_MetaPropertyManager = metaPropertyManager;
                m_MetaProperty = metaProperty;

                var synchronizable = (ISynchronizable)m_MetaPropertyManager.GetProperty(metaProperty);
                synchronizable.IsSynchronizing = true;
            }

            public void Dispose()
            {
                var synchronizable = (ISynchronizable)m_MetaPropertyManager.GetProperty(m_MetaProperty);
                synchronizable.IsSynchronizing = false;
            }
        }

        #endregion
    }
}
